// ==UserScript==
// @name           	WM Common Library
// @description	A collection of useful functions and objects, some of which are specific to the Wall Manager family of scripts.
// @require		http://userscripts.org/scripts/source/123889.user.js
// @license		http://creativecommons.org/licenses/by-nc-nd/3.0/us/
// @version        	3.0.0.2
// @copyright       Charlie Ewing except where noted
// ==/UserScript== 

//this script needs access to a pre-defined JSON object

(function(){
var sandbox=this;

// is Greasemonkey running
sandbox.isGM = (typeof GM_getValue != 'undefined' && typeof GM_getValue('a', 'b') != 'undefined');
sandbox.isChrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;

//return true if o is undefined
sandbox.isUndefined=function(o){try{return ((typeof o)=="undefined");}catch(e){log("wmLibrary.isUndefined: "+e);}};

//return true if o is a string
sandbox.isString=function(o){try{return ((typeof o)=="string");}catch(e){log("wmLibrary.isString: "+e);}};

//return true if o is not undefined
sandbox.exists=function(o){try{return (!isUndefined(o));}catch(e){log("wmLibrary.exists: "+e);}};

// Returns true if object o is an array
sandbox.isArray=function(o){try{return (o instanceof Array);}catch(e){log("wmLibrary.isArray: "+e);}};

// Returns true if object o is an object but not an array
sandbox.isObject=function(o){try{return (((typeof o)=="object") && !isArray(o));}catch(e){log("wmLibrary.isObject: "+e);}};

// cross-browser log function, turns the log variable into a function
// originally from FVWM by Joe Simmons
// now also catches the WM debug window first
sandbox.log=function(){try{ 
	var fx, debug=this.debug;
	if (exists(debug)) fx=debug.print;
	else if (isGM) fx=GM_log;
	else if (window.opera) fx=opera.postError;
	else fx=console.log;
	if (fx) {var args=arguments, self=this; setTimeout(function(){fx.apply(self,args);},0); }
}catch(e){console.log("WmLibrary.log: "+e);}};

//return a DOM element by ID with optional alternate root document
sandbox.$=function(ID,root) {try{return (root||document).getElementById(ID);}catch(e){log("wmLibrary.$: "+e);}};

//append css style to the header
sandbox.addGlobalStyle=function(css,name,doc) {try{var head, style;head = (doc||document).getElementsByTagName('head')[0];if (!head) { return; };style = (doc||document).createElement('style');style.type = 'text/css';style.innerHTML = css;head.appendChild(style); if(name||null) style.name=name;}catch(e){log("wmLibrary.addGlobalStyle: "+e);}};

//click specified DOM element
sandbox.click=function(e) {try{if(!e && typeof e=='string') e=document.getElementById(e);if(!e) return;var evObj = e.ownerDocument.createEvent('MouseEvents');evObj.initMouseEvent("click",true,true,e.ownerDocument.defaultView,0,0,0,0,0,false,false,false,false,0,null);e.dispatchEvent(evObj);}catch(e){log("wmLibrary.click: "+e);}};

//return new DOM element a, with parameters b, and children c
sandbox.createElement=function(a,b,c) {try{if(a=="text") {return document.createTextNode(b);};var ret=document.createElement(a.toLowerCase());if(b) for(var prop in b) if(prop.indexOf("on")==0) ret.addEventListener(prop.substring(2),b[prop],false);else if(",style,accesskey,id,name,src,href,which,rel,action,method,value,data-ft".indexOf(","+prop.toLowerCase())!=-1) ret.setAttribute(prop.toLowerCase(), b[prop]);else ret[prop]=b[prop];if(c) c.forEach(function(e) { if (e) ret.appendChild(e); });return ret;}catch(e){log("wmLibrary.createElement: "+e);}};

//return document.location.pathname
sandbox.getDocName=function() {try{return document.location.pathname;}catch(e){log("wmLibrary.getDocName: "+e);}};

//remove specified DOM element
sandbox.remove=function(e) {try{var node=(typeof e=='string')?$(e):e; if(node) node.parentNode.removeChild(node); node=null;}catch(e){log("wmLibrary.remove: "+e);}};

//return a unix timestamp
sandbox.timeStamp=function(){try{return (new Date()).getTime();}catch(e){log("wmLibrary.timeStamp: "+e);}};

//return selected nodes using xpath, with additional parameters
sandbox.selectNodes=function(xPath,params){try{params=(params||{});var doc = (params.doc||document), node = (params.node||doc); return doc.evaluate(xPath,node,null,(params['type']||6),null);}catch(e){log("wmLibrary.selectNodes: "+e);}};

//return single selected node using xpath, with additional parameters
sandbox.selectSingleNode=function(xPath,params){try{params=params||{}; params['type']=9;return selectNodes(xPath,params).singleNodeValue;}catch(e){log("wmLibrary.selectSingleNode: "+e);}};

//for the selected nodes using xpath and additional parameters, perform passed function
sandbox.forNodes=function(xPath,params,fx){try{if(!fx) return;var nodes = selectNodes(xPath,params);if (nodes.snapshotLength) {for (var i=0,node;(node=nodes.snapshotItem(i));i++) {fx(node);}}nodes=null;}catch(e){log("wmLibrary.forNodes: "+e);}};

//return true if string starts with s
sandbox.String.prototype.startsWith = function(s) {try{if (this.length<s.length) return false; else return (this.substring(0,s.length)===s)}catch(e){log("wmLibrary.String.prototype.startsWith: "+e);}};

//return true if string ends with s
sandbox.String.prototype.endsWith = function(s) {try{if (this.length<s.length) return false; else return (this.substring(this.length-s.length,s.length)===s)}catch(e){log("wmLibrary.String.prototype.endsWith: "+e);}};

//return true if string contains s
sandbox.String.prototype.find = function(s) {try{return (this.indexOf(s) != -1);}catch(e){log("wmLibrary.String.prototype.find: "+e);}};
sandbox.String.prototype.contains = function(s) {try{return (this.indexOf(s) != -1);}catch(e){log("wmLibrary.String.prototype.contains: "+e);}};

//return the passed string minus spaces
sandbox.String.prototype.noSpaces = function(s) {try{return (this.replace(/\s+/g,''));}catch(e){log("wmLibrary.String.prototype.noSpaces: "+e);}};

//return the passed string with word first letters capitalized
sandbox.String.prototype.upperWords = function(s) {try{return (this+'').replace(/^(.)|\s(.)/g, function($1){return $1.toUpperCase();});}catch(e){log("wmLibrary.String.prototype.upperWords: "+e);}};

//return the passed string repeated n times
sandbox.String.prototype.repeat = function(n) {try{return new Array(n+1).join(this);}catch(e){log("wmLibrary.String.prototype.repeat: "+e);}};

//return the passed string minus line breaks
sandbox.String.prototype.noLineBreaks = function(s) {try{return (this.replace(/(\r\n|\n|\r)/gm," "));}catch(e){log("wmLibrary.String.prototype.noLineBreaks: "+e);}};

//return the passed string without beginning or ending quotes
sandbox.String.prototype.unQuote = function() {try{return this.replace(/^"|"$/g, '');}catch(e){log("wmLibrary.String.prototype.unQuote: "+e);}};

//return the passed string without beginning or ending brackets
sandbox.String.prototype.unBracket = function() {try{return this.replace(/^\[|\]$/g, '');}catch(e){log("wmLibrary.String.prototype.unBracket: "+e);}};

//return the passed string without beginning or ending spaces
sandbox.String.prototype.trim = function(){try{return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');}catch(e){log("wmLibrary.String.prototype.trim: "+e);}};

//assuming passed string is a url parameter list, return named parameter's value or ""
sandbox.String.prototype.getUrlParam = function(s) {try{
	var r="{\"" + this.replace(/&/g,"\",\"").replace(/=/g,"\":\"") + "\"}";
	var o=JSON.parse(r,{});
	var m=(o[s]||"");
	return m;
}catch(e){log("wmLibrary.String.prototype.getUrlParam: "+e);}};

//return passed string with word added to end. words are separated by spaces
sandbox.String.prototype.addWord= function(word){try{var words = this.split(" ");if (!words.inArray(word)) return this+" "+word;return this;}catch(e){log("wmLibrary.String.prototype.addWord: "+e);}};

//return passed string minus specified word
sandbox.String.prototype.removeWord= function(word){try{return this.split(" ").removeByValue(word).join(" ");}catch(e){log("wmLibrary.String.prototype.removeWord: "+e);}};

//return true if passed string contains word
sandbox.String.prototype.containsWord= function(word){try{return this.split(" ").inArray(word);}catch(e){log("wmLibrary.String.prototype.containsWord: "+e);}};

//return passed string with word replaced with word2
sandbox.String.prototype.replaceWord= function(word,word2){try{return this.split(" ").replace(word,word2).join(" ");}catch(e){log("wmLibrary.String.prototype.replaceWord: "+e);}};

//return passed string with word toggled
sandbox.String.prototype.toggleWord= function(word){try{if (this.containsWord(word)) return this.removeWord(word); return this.addWord(word);}catch(e){log("wmLibrary.String.prototype.toggleWord: "+e);}};

//return passed string minus prefix of s if it exists
sandbox.String.prototype.removePrefix = function(s){try{if (this.startsWith(s)) {return this.substring(s.length);} else return this;}catch(e){log("wmLibrary.String.prototype.removePrefix: "+e);}};

//return passed string minus suffix of s if it exists
sandbox.String.prototype.removeSuffix = function(s){try{if (this.endsWith(s)) {return this.substring(0,this.length-s.length);} else return this;}catch(e){log("wmLibrary.String.prototype.removeSuffix: "+e);}};

//return passed array with element x and element y swapped
sandbox.Array.prototype.swap = function (x,y) {try{var b = this[x];this[x] = this[y];this[y] = b;return this;}catch(e){log("wmLibrary.Array.prototype.swap: "+e);}};

//return true if a value exists in the array
sandbox.Array.prototype.inArray = function(value) {try{for(var i=this.length-1; i>=0; i--) {if(this[i]==value) return true;} return false;}catch(e){log("wmLibrary.Array.prototype.inArray: "+e);}};

//return the location of a value in an array
sandbox.Array.prototype.inArrayWhere = function(value) {try{for(var i=0,l=this.length; i<l; i++) {if(this[i]==value) return i;}return -1;}catch(e){log("wmLibrary.Array.prototype.inArrayWhere: "+e);}};

//return the last value in an array
sandbox.Array.prototype.last = function() {try{return this[this.length - 1];}catch(e){log("wmLibrary.Array.prototype.last: "+e);}};

//return the array will spaces removed from every element
sandbox.Array.prototype.noSpaces = function() {try{for(var i=0,l=this.length; i<l; i++) {this[i]=this[i].noSpaces();}; return this;}catch(e){log("wmLibrary.Array.prototype.noSpaces: "+e);}};

//remove the first instance of a value in an array
sandbox.Array.prototype.removeByValue = function(val) {try{var i=this.inArrayWhere(val);if(i>=0)this.splice(i,1);return this;}catch(e){log("wmLibrary.Array.prototype.removeByValue: "+e);}};

//replace the first instance of a value in an array
sandbox.Array.prototype.replace = function(val, val2) {try{var i=this.inArrayWhere(val);if(i>=0)this[i]=val2;return this;}catch(e){log("wmLibrary.Array.prototype.replace: "+e);}};

//remove element i of an array
sandbox.Array.prototype.remove = function(i) {try{this.splice(i,1); return this;}catch(e){log("wmLibrary.Array.prototype.remove: "+e);}};

//return a random element of an array
sandbox.Array.prototype.pickRandom = function () {try{var i=Math.floor(Math.random()*this.length); return this[i];}catch(e){log("wmLibrary.Array.prototype.pickRandom: "+e);}};

//enumerated string equal to script that does nothing
sandbox.jsVoid="javascript:void(0)";

//sorts an array by string length, priming it for text searches, putting longest strings first so that "pea" is not found before "peanut"
sandbox.Array.prototype.optimize = function(){try{if (this.length>1) {for (var i=this.length-1;i>0;i--) {for (var i2=i-1;i2>0;i2--){if (this[i].length > this[i2].length) {var b=this[i]; this[i]=this[i2]; this[i2]=b; b=null;}}}}; return this;}catch(e){log("wmLibrary.Array.prototype.optimize: "+e);}};

//returns the merge of any number of JSON objects passed as unnamed arguments
sandbox.mergeJSON_long = function(){try{
	var ret = {}; 
	//for each JSON object passed
	for (var a=0,len=arguments.length;a<len;a++) {
		//for each element in that object
		for (var v in arguments[a]) {
			if (!exists(ret[v])) {
				//simply copy the element to the return value
				ret[v] = arguments[a][v];
			} else {
				if ((typeof arguments[a][v])=="object") {
					//merge the two elements, preserving tree structure
					ret[v] = mergeJSON(ret[v], arguments[a][v]);
				} else {
					//overwrite simple variable
					ret[v] = arguments[a][v];
				}
			}
		}
	}
	//the problem here is that its way too recursive and jams firefox often
	return ret; 
}catch(e){log("wmLibrary.mergeJSON: "+e);}};

sandbox.mergeJSON = function(){try{
	var ret = {}; 
	//for each JSON object passed
	for (var a=0,len=arguments.length;a<len;a++) {
		//for each element in that object
		for (var v in arguments[a]) {
			//replace the initial element with that of the next
			ret[v] = arguments[a][v];
		}
		//the problem here is that only the top level branches are preserved
	}
	return ret; 
}catch(e){log("wmLibrary.mergeJSON: "+e);}};

//returns an object suitable for accText data based on an array, and allowing an idPrefix and textSuffix
sandbox.createAccTextFromArray = function(arr,idPrefix,textSuffix){try{var ret={};if (arr) {for (var i=0,len=arr.length;i<len;i++){ret[(idPrefix||'')+arr[i].noSpaces().toLowerCase()]=arr[i].upperWords()+(textSuffix||'');}};return ret;}catch(e){log("wmLibrary.createAccTextFromArray: "+e);}};

//writes a message to the hash section of the document location, or redirects to a location that can accept a new hash section
sandbox.sendMessage=function(s,hwnd){try{
	hwnd = (hwnd||window.top);
	if (exists(hwnd)) try {hwnd.location.hash = s;} catch(e){hwnd.location.href = "http://apps.facebook.com/?#"+s;}
}catch(e){log("wmLibrary.sendMessage: "+e);}};

//flags for menu building function
sandbox.MENU_ID_ENFORCE_NAME=1; //causes menuFromData to return lowercase nospace names as the id instead of the calculated id

//inserts one or more menu option blocks based upon a data object
//marking all new items in the newitem list above as green so users can easily find your changes
sandbox.menuFromData=function(data,menuNode,newItemList,idPrefix,flags){try{
	flags=(flags||0); newItemList=(newItemList||[]);
	if (data) for (var m=0,len=data.length; m<len; m++) {
		var text = data[m]["name"].upperWords(), event = (data[m]["event"]||"Unsorted").upperWords();
		var outid = (flags==MENU_ID_ENFORCE_NAME)?data[m].name.noSpaces().toLowerCase():(data[m]["id"]||data[m]["name"]).noSpaces().toLowerCase();
		var thisMenu; if( !(thisMenu=(menuNode["optblock"+event]||null) ) ) {thisMenu=(menuNode["optblock"+event]={type:"optionblock",label:event,kids:{} });}
		thisMenu.kids[idPrefix+outid]={type:"checkbox",label:text,newitem:newItemList.inArray(idPrefix+outid)};
	}
}catch(e){log("wmLibrary.menuFromData: "+e);}};

//returns a list of search strings from a data object containing id's names and events, already optimized for searching
sandbox.searchFromData=function(data,idPrefix){try{
	idPrefix=(idPrefix||""); 
	var ret = []; 
	for (var m=0,mat;(mat=data[m]);m++){
		ret.push(idPrefix+(mat.id||mat.name));
	} 
	ret.optimize();
	return ret;
}catch(e){log("wmLibrary.searchFromData: "+e);}};

//returns a list of materials from a data object containing id's names and events, already optimized for searching
sandbox.matListFromData=function(data){try{
	var ret = []; 
	for (var m=0,mat;(mat=data[m]);m++){
		ret.push(mat.name);
	} ret.optimize(); 
	return ret;
}catch(e){log("wmLibrary.matListFromData: "+e);}};

//returns a valid accText object from a data object containing id's names and events
sandbox.accTextFromData=function(data,idPrefix,textSuffix,flags){try{idPrefix=(idPrefix||""); textSuffix=(textSuffix||"");var ret={}; for (var m=0,mat;(mat=data[m]);m++){ret[idPrefix+((flags==MENU_ID_ENFORCE_NAME)?mat.name:(mat.id||mat.name)).noSpaces().toLowerCase()]=(mat.name+textSuffix).upperWords();} return ret;}catch(e){log("wmLibrary.accTextFromData: "+e);}};

//sidekick specific functions
sandbox.Sidekick={	
	//init
	tabID:null,
	status:0,
	nopopLink:"",

	//attempts to dock the sidekick script to the wm host script
	//params takes an object that contains the following parameters: 
	//appID(string), synAppID(string or array), version(string), name(string), thumbSource(string or array), flags(object), icon(string), desc(string), alterLink(object), accText(object), tests(array) and menu(object)
	dock: function(params){try{
		var door=$('wmDock');if (!door) {window.setTimeout(function(){Sidekick.dock(params);}, 1000);return;} 
		var doorMark=$('wmDoor_app'+params.appID); if (doorMark) return;
		params.thumbsSource=(params.thumbsSource||"app_full_proxy.php?app"+params.appID);
		params.desc=(params.desc||params.name+" Sidekick (ver "+params.version+")");
		var attString=JSON.stringify(params);
		door.appendChild(createElement('div',{id:'wmDoor_app'+params.appID,'data-ft':attString}));
		window.setTimeout(function(){click(door);},1000);
	}catch(e){log("wmLibrary.Sidekick.dock: "+e);}},
	
	
	//receive and process messages
	//msg code 1 is a packet from the wm host containing data about the post we are processing
	//that packet must contain at least the tab/window ID with which the WM host can access that tab again
	//msg code 3 is a packet from this or a deeper iframe window about the return value for this post
	
	//because Chrome returns NULL at event.source on msg 1, we now have to rethink
	receiveMessage: function(event) {try{
		if (isObject(event.data)) {
			var data=event.data; //just shorten the typing
			if (data.channel=="WallManager"){
				log(JSON.stringify(data));
				
				switch (data.msg) {
					case 1: //get init data from wm host
						//if (!Sidekick.tabID) {
							Sidekick.tabID=data.tabID;
							log("Sidekick hears host...");
						//}
						break;
					case 3: //get message from child
						if (Sidekick.tabID) {
							log("Sidekick hears iframe...");
							//send our status packet back to wm
							Sidekick.status=data.status;
							Sidekick.nopopLink=data.nopopLink||null;
							//update the stored data about this post
							var skChannel = getOptJSON("skChannel")||{};
							skChannel[Sidekick.tabID]={
								tabID:Sidekick.tabID,
								status:Sidekick.status,
								nopopLink:Sidekick.nopopLink,
							};
							log(JSON.stringify(skChannel));
							setOptJSON("skChannel",skChannel);
						} else {
							//have not yet recieved tabID package from wm, wait a sec
							setTimeout(function(){Sidekick.receiveMessage(event);},1000);
						}
						break;
				}
			}
		}
	}catch(e){log("wmLibrary.Sidekick.receiveMessage: "+e);}},

	//disable the listener started below
	unlisten: function(params){try{
		window.removeEventListener("message", Sidekick.receiveMessage, false);
	}catch(e){log("wmLibrary.Sidekick.unlisten: "+e);}},
	
	//turn on the listener which can receive messages from wm host (if this window = window.top) or from iframes
	listen: function(params){try{
		window.addEventListener("message", Sidekick.receiveMessage, false);
	}catch(e){log("wmLibrary.Sidekick.listen: "+e);}},

	//listen for changes to the skChannel variable and report those changes to WM whenever docked
	openChannel: function(){try{
		var dump=$("wmDataDump");
		if (dump) {
			var skData=getOpt("skChannel");
			setOpt("skChannel","");
			if (skData) dump.appendChild(createElement('div',{'data-ft':skData}));
		}
		setTimeout(Sidekick.openChannel,1000);
	}catch(e){log("wmLibrary.Sidekick.openChannel: "+e);}},

	//send a status code from the deepest iframe to the topmost frame so that it can be passed back with data the top window already has
	sendStatus: function(status,link){try{
		if (exists(window.top)) {
			window.top.postMessage({
				channel:"WallManager",
				msg:3,
				status:status,
				nopopLink:(link?link:''),
			},"*");
		} else {
			//window.top is hidden to us from this location
			contentEval('window.top.postMessage({"channel":"WallManager","msg":3,"status":'+status+',"link":"'+(link?link:'')+'"},"*");');
		}	
	}catch(e){log("wmLibrary.Sidekick.sendStatus: "+e);}},
};

//returns all members of an array that have a specified parameter with a specified value
sandbox.matchByParam=function(arr,param,value){try{var ret=[];for (var i=0,e;(e=arr[i]);i++){if (e[param]==value) ret.push(e);};return ret;}catch(e){log("wmLibrary.matchByParam: "+e);}};







//return integer value of object
sandbox.val=function(o){try{return parseInt(o);}catch(e){log("wmLibrary.val: "+e);}};



//returns a guaranteed unique timestamp in base36 prefixed with an underscore
sandbox.unique=function(){try{var now=timeStamp();var newnow=now;while (newnow==now){newnow=timeStamp();} return "_"+(newnow.toString(36));}catch(e){log("wmLibrary.unique: "+e);}};

//time enums
sandbox.second=1000;
sandbox.minute=second*60;
sandbox.hour=minute*60;
sandbox.day=hour*24;


//slides element e toward the specified destination offset
//specify [t, l, r, b] top, left, right, and bottom as the final offset
//specify s as the number of MS the move should loop on
//specify p as the number of pixels to move per interval
sandbox.slide=function(e,t,l,r,b,s,p) {try{
	s=s||50;p=p||10;

	var top= e.style.top; top=parseInt(top); top=(isNaN(top))?0:top;
	var bottom = e.style.bottom; bottom=parseInt(bottom); bottom=(isNaN(bottom))?0:bottom;
	var left= e.style.left; left=parseInt(left); left=(isNaN(left))?0:left;
	var right = e.style.right; right=parseInt(right); right=(isNaN(right))?0:right;

	p1=(p>Math.abs(t))?Math.abs(t):p;
	if(t>0) {e.style.top = (top+p1)+"px";t-=p1;}
	else if (t<0) {e.style.top = (top-p1)+"px";t+=p1;}

	p1=(p>Math.abs(l))?Math.abs(l):p;
	if(l>0) {e.style.left = (left+p1)+"px";l-=p1;}
	else if (l<0) {e.style.left = (left-p1)+"px";l+=p1;}

	p1=(p>Math.abs(r))?Math.abs(r):p;
	if(r>0) {e.style.right = (right+p1)+"px";r-=p1;}
	else if (r<0) {e.style.right = (right-p1)+"px";r+=p1;}

	p1=(p>Math.abs(b))?Math.abs(b):p;
	if(b>0) {e.style.bottom = (bottom+p1)+"px";b-=p1;}
	else if (b<0) {e.style.bottom = (bottom-p1)+"px";b+=p1;}

	if (t!=0||l!=0||r!=0||b!=0) window.setTimeout(function(){slide(e,t,l,r,b,s,p);},s);
}catch(e){log("wmLibrary.slide: "+e);}};

//url encode/decode functions nicely wrapped from webtoolkit
sandbox.Url = {
	// public method for url encoding
	encode : function (string) {try{return escape(this._utf8_encode(string));}catch(e){log("wmLibrary.Url.encode: "+e);}},
 
	// public method for url decoding
	decode : function (string) {try{return this._utf8_decode(unescape(string));}catch(e){log("wmLibrary.Url.decode: "+e);}},
 
	// private method for UTF-8 encoding
	_utf8_encode : function (string) {
		string = string.replace(/\r\n/g,"\n");
		var utftext = "";
		for (var n = 0; n < string.length; n++) {
			var c = string.charCodeAt(n);

			if (c < 128) {
				utftext += String.fromCharCode(c);
			}
			else if((c > 127) && (c < 2048)) {
				utftext += String.fromCharCode((c >> 6) | 192);
				utftext += String.fromCharCode((c & 63) | 128);
			}
			else {
				utftext += String.fromCharCode((c >> 12) | 224);
				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
				utftext += String.fromCharCode((c & 63) | 128);
			}
		}
		return utftext;
	},
 
	// private method for UTF-8 decoding
	_utf8_decode : function (utftext) {
		var string = "";
		var i = 0;
		var c = c1 = c2 = 0;
		while ( i < utftext.length ) {
			c = utftext.charCodeAt(i);
			if (c < 128) {
				string += String.fromCharCode(c);
				i++;
			}
			else if((c > 191) && (c < 224)) {
				c2 = utftext.charCodeAt(i+1);
				string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
				i += 2;
			}
			else {
				c2 = utftext.charCodeAt(i+1);
				c3 = utftext.charCodeAt(i+2);
				string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
				i += 3;
			} 
		}
		return string;
	}
};





//return the real url of a location
sandbox.realURL=function() {try{var u=window.location.href, host=window.location.host, protocol=window.location.protocol+"//", hash=window.location.hash;if(hash!="" && (/#\/.*\.php/).test(hash)) u=protocol+host+hash.split("#")[1];else if(hash!="" && hash.find("#")) u=u.split("#")[0];if (u.substr(-1) === "#") u=u.split("#")[0];return u;}catch(e){log("wmLibrary.realURL: "+e);}};

//attach an array of elements to a node
sandbox.appendChildren = function(node,arr){try{for (var i=0,len=arr.length;i<len;i++){node.appendChild(arr[i]);};}catch(e){log("wmLibrary.appendChildren: "+e);}};

//create a set of options for a selection list based on an array
sandbox.optionsFromArray = function(arr){try{var ret=[];for (var i=0,len=arr.length;i<len;i++) {ret.push(createElement("option",{value:arr[i],textContent:arr[i]}));};return ret;}catch(e){log("wmLibrary.optionsFromArray: "+e);}};

//select an element from a dropdown box with a certain value
sandbox.selectDropDownElement = function(obj,value){try{var node = selectSingleNode(".//option[@value='"+value+"']",{node:obj});if (node) node.selected=true;}catch(e){log("wmLibrary.selectDropDownElement: "+e);}};

//return the value of a dropdown's selected inded
sandbox.valueOfSelect = function(obj){try{return obj.options[obj.selectedIndex].value;}catch(e){log("wmLibrary.valueOfSelect: "+e);}};

//hides all snapshots or iterations in an xpathResult object
sandbox.hideNodes=function(xPath,params) {try{forNodes(xPath,params,function(item){item.style.display="none";});}catch(e){log("wmLibrary.hideNodes: "+e);}};

//unhides all snapshots or iterations in an xpathResult object
sandbox.showNodes=function(xPath,params) {try{forNodes(xPath,params,function(item){item.style.display="";});}catch(e){log("wmLibrary.showNodes: "+e);}};

// set an option
sandbox.setOpt=function(opt,value){try{GM_setValue(opt,value);}catch(e){log("wmLibrary.setOpt: "+e);}}

// Get a stored option
sandbox.getOpt=function(opt){try{return GM_getValue(opt);}catch(e){log("wmLibrary.getOpt: "+e);}}

// set an option
sandbox.setOptJSON=function(opt,value){try{GM_setValue(opt,JSON.stringify(value));}catch(e){log("wmLibrary.setOptJSON: "+e);}}

// Get a stored option
sandbox.getOptJSON=function(opt){try{var val=GM_getValue(opt, '{}');return JSON.parse(val);}catch(e){log("wmLibrary.getOptJSON: "+e);}}

// Collect all the values from parameter p in object o, traversing kids nodes
sandbox.getBranchValues=function(o,p){try{
	var ret={};
	for(var i in o) {
		//get value p for object o's element i
		if (p=="id"){ //special case for fetching a list of ID's
			if (exists(o[i][p])) ret[i]=o[i][p];
			else ret[i]=i;
		} else if (p=="."){ //special case for fetching a list of all objects without a tree structure
			ret[i]=o[i];
		}

		else if (exists(o[i][p])) ret[i]=o[i][p];
		//if object o has kids, then get all the values p inside that kid k
		if (o[i].kids) ret=mergeJSON(ret,getBranchValues(o[i].kids,p));
	}
	return ret;
}catch(e){log("wmLibrary.getBranchValues: "+e);}};

// visual basic alternate for string.toLowerCase()
sandbox.String.prototype.lcase = function() {try{return this.toLowercase();}catch(e){log("wmLibrary.String.prototype.lcase: "+e);}};

// visual basic alternate for string.toUpperCase()
sandbox.String.prototype.ucase = function() {try{return this.toUppercase();}catch(e){log("wmLibrary.String.prototype.ucase: "+e);}};
	
// copy the calling string to the clipboard (IE or GM)
sandbox.String.prototype.toClipboard = function() {try{
	if (window.clipboardData){  
		window.clipboardData.setData("Text", this);
	} else if (unsafeWindow) {  
		try{
			unsafeWindow.netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
		} catch(e){
			log("wmLibrary.String.prototype.toClipboard: Cannot enable privelege 'UniversalXPConnect'. Be sure that 'signed.applets.codebase_principal_support' is set to true in 'about:config'");
		}
		const clipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"].getService(Components.interfaces.nsIClipboardHelper);  
		clipboardHelper.copyString(this);
	} else {
		log("wmLibrary.String.prototype.toClipboard: Cannot perform task");
	}
} catch(e){log("wmLibrary.String.prototype.toClipboard: "+e);}};

// compile and return the true x,y scroll offset onscreen of an element in Firefox	
sandbox.trueScrollOffset = function(o){try{
	var offset={left:o.scrollLeft,top:o.scrollTop}, parentOffset=null;
	if (!(o==document.body) && !(0==document.documentElement) && o.parentNode) parentOffset=trueScrollOffset(o.parentNode);
	if (parentOffset) {
		offset.left+=parentOffset.left||0;
		offset.top+=parentOffset.top||0;
	}
	return offset;
}catch(e){log("wmLibrary.trueScrollOffset: "+e);}},

// compile and return the true x,y offset onscreen of an element in Firefox
sandbox.trueOffset = function(o){try{
	var offset={left:o.offsetLeft,top:o.offsetTop}, parentOffset=null;
	if (o.offsetParent) parentOffset=trueOffset(o.offsetParent);
	if (parentOffset) {
		offset.left+=parentOffset.left||0;
		offset.top+=parentOffset.top||0;
	}
	return offset;
}catch(e){log("wmLibrary.trueOffset: "+e);}},

// add two points or vectors
sandbox.addPoints = function(p0, p1){try{
	var p2=mergeJSON(p0); //copy p0
	for (var v in p1) p2[v]=(p2[v]||0)+(p1[v]||0);
	return p2;
}catch(e){log("wmLibrary.addPoints: "+e);}},

// shortform for window.setTimeout(x,0)
sandbox.doAction = function(f) {try{setTimeout(f,0);}catch(e){log("doAction: "+e);}};

// create an unprotected enumeration list
sandbox.Enum = function() {try{for (var i in arguments) {this[arguments[i]] = i;}}catch(e){log("Enum.init: "+e);}};

//create an unprotected enumeration list of binary flags
sandbox.EnumFlags = function() {try{for (var i in arguments) {this[arguments[i]] = Math.pow(2,i);}}catch(e){log("EnumFlags.init: "+e);}};

//convert an object's methods to an array, storing the method's key on the object as an id
sandbox.methodsToArray = function(o) {try{var ret=[]; for (var i in o) {o[i].id=o[i].id||i; ret.push(o[i])}; return ret;}catch(e){log("methodsToArray: "+e);}};

//convert an array of objects to methods of an object using either the object's ai or name as its key
sandbox.arrayToMethods = function(a) {try{var ret={}; for (var i=0;i<a.length;i++) ret[ a[i].id||a[i].name ]=a[i]; return ret;}catch(e){log("arrayToMethods: "+e);}};

//convert an object's methods to an array of those method names
sandbox.methodNames = function(o) {try{var ret=[]; for (i in o) ret.push(i); return ret;}catch(e){log("methodNames: "+e);}};

//returns an array containing elements from the current array where the element has parameter p equal to value v
sandbox.Array.prototype.selectByParam = function(p,v) {try{var ret=[]; for(i=0;i<this.length;i++) if(this[i][p]==v) ret.push(this[i]); return ret;}catch(e){log("Array.prototype.selectByParam: "+e);}};

//create a centered iframe to display multiline text in a textarea
sandbox.promptText = function(s){try{
	var newFrame;
	document.body.appendChild((newFrame=createElement('iframe',{style:'position:fixed; top:0; left:0; display:none !important; z-index:999; width:75%; height:75%; max-height:95%; max-width:95%; border:1px solid #000000; overflow:auto; background-color:white;'})));
	newFrame.src = 'about:blank'; // In WebKit src cant be set until it is added to the page
	newFrame.addEventListener('load', function(){
		var frameBody = this.contentDocument.getElementsByTagName('body')[0];
		
		var close=function(){try{
			remove(newFrame);
			delete newFrame;
		}catch(e){log("smLibrary.promptText.close: "+e);}};
		
		// Add save and close buttons
		frameBody.appendChild(
			createElement("pre",{textContent:s})
		);
		frameBody.appendChild(
			createElement("div", {id:"buttons_holder"}, [
				createElement('button',{id:"closeBtn", textContent:"Close",title:"Close window",onclick:close}),
			])
		);
		
		var center=function(){try{
			var style=newFrame.style;
			var node=newFrame;
			style.display = '';
			style.top = Math.floor((window.innerHeight/2)-(node.offsetHeight/2)) + 'px';
			style.left = Math.floor((window.innerWidth/2)-(node.offsetWidth/2)) + 'px';
		}catch(e){log("wmLibrary.promptText.center: "+e);}};
		center();				
		window.addEventListener('resize', center, false); // Center it on resize

		// Close frame on window close
		window.addEventListener('beforeunload', function(){newFrame.remove(this);}, false);
		
	}, false);
}catch(e){log("wmLibrary.promptText: "+e);}};

sandbox.linkTo = function(s) {try{
	var link=document.body.appendChild(createElement("a",{href:s,target:"_top"}));
	click(link);
}catch(e){log("wmLibrary.linkTo: "+e);}};

//repeat a function fn a number of times n with a delay of 1 second between calls
sandbox.signal = function(fn,n){try{
	if (n>0) {
		doAction(fn);
		setTimeout(function(){signal(fn,n-1);},1000);
	}
}catch(e){log("wmLibrary.signal: "+e);}};

//force code to be run outside the GM sandbox
sandbox.contentEval = function(source) {try{
	// Check for function input.
	if ('function' == typeof source) {
	// Execute this function with no arguments, by adding parentheses.
	// One set around the function, required for valid syntax, and a
	// second empty set calls the surrounded function.
		source = '(' + source + ')();'
	}

	// Create a script node holding this  source code.
	var script = document.createElement('script');
	script.setAttribute("type", "application/javascript");
	script.textContent = source;

	// Insert the script node into the page, so it will run, and immediately
	// remove it to clean up.
	document.body.appendChild(script);
	document.body.removeChild(script);
}catch(e){log("wmLibrary.contentEval: "+e);}};

//comprehensive convert anything to a boolean value
sandbox.cBool = function(val){try{
	//capture boolean values
	if ((typeof val)=="boolean") return val;
	//capture undefined
	if (!exists(val)) return false;
	//capture checkboxes
	if (exists(val.checked)) val=val.checked;
	//capture objects with value property
	if (exists(val.value)) val=val.value;
	//capture non-null objects
	if (isObject(val)) return true;
	//capture arrays
	if (isArray(val)) return true;
	//capture text
	if (typeof val=="string") switch(val.toLowerCase()){
		case "1": case "true": case "yes": return true; break;
		case "0": case "false": case "no": return false; break;
	}
	//default
	return Boolean(val);
}catch(e){log("wmLibrary.cBool: "+e);}};

//copy parts from one object to another
//used for extending one object with parts from another
//by John Resig
sandbox.extend = function(a,b) {try{
	for ( var i in b ) {
		//collect setter/getter functions
		var g = b.__lookupGetter__(i), s = b.__lookupSetter__(i);
		//copy setter/getter functions
		if ( g || s ) {
			if ( g ) a.__defineGetter__(i, g);
			if ( s ) a.__defineSetter__(i, s);
		} else a[i] = b[i]; //copy vars
	}
	return a;
}catch(e){log("wmLibrary.extend: "+e);}};	

})();